package com.ycy.dubbo.entity;


import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Pattern;

/**
 * Central class for creating queries. It follows a fluent API style so that you can easily chain together multiple
 * SearchQuery. Static import of the 'SearchQuery.where' method will improve readability.
 */
public class SearchQuery implements Serializable {

    /**
     * Custom "not-null" object as we have to be able to work with {@literal null} values as well.
     */

    private String key;
    private List<SearchQuery> SearchQueryChain;
    private LinkedHashMap<String, Object> SearchQuery = new LinkedHashMap<String, Object>();
    private DocumentQuery searchQueryObject;

    private ArrayList isValue = null;

    public SearchQuery() {
        this.SearchQueryChain = new ArrayList<SearchQuery>();
    }

    public SearchQuery(String key) {
        this.SearchQueryChain = new ArrayList<SearchQuery>();
        this.SearchQueryChain.add(this);
        this.key = key;
    }

    protected SearchQuery(List<SearchQuery> SearchQueryChain, String key) {
        this.SearchQueryChain = SearchQueryChain;
        this.SearchQueryChain.add(this);
        this.key = key;
    }

    /**
     * Static factory method to create a SearchQuery using the provided key
     *
     * @param key
     * @return
     */
    public static SearchQuery where(String key) {
        return new SearchQuery(key);
    }

    /**
     * Static factory method to create a SearchQuery using the provided key
     *
     * @return
     */
    public SearchQuery and(String key) {
        return new SearchQuery(this.SearchQueryChain, key);
    }

    /**
     * Creates a criterion using equality
     *
     * @param o
     * @return
     */
    public SearchQuery is(Object o) {

        if (isValue != null) {
            throw new RuntimeException(
                    "Multiple 'is' values declared. You need to use 'and' with multiple SearchQuery");
        } else {
            this.isValue = new ArrayList(1);
        }


        if (lastOperatorWasNot()) {
            throw new RuntimeException("Invalid queryProcInsts: 'not' can't be used with 'is' - use 'ne' instead.");
        }


        isValue.add(o);
        return this;
    }

    private boolean lastOperatorWasNot() {
        return !this.SearchQuery.isEmpty() && "$not".equals(this.SearchQuery.keySet().toArray()[this.SearchQuery.size() - 1]);
    }

    /**
     * Creates a criterion using the {@literal $ne} operator.
     *
     * @param o
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/ne/">MongoDB Query operator: $ne</a>
     */
    public SearchQuery ne(Object o) {
        SearchQuery.put("$ne", o);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $lt} operator.
     *
     * @param o
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/lt/">MongoDB Query operator: $lt</a>
     */
    public SearchQuery lt(Object o) {
        SearchQuery.put("$lt", o);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $lte} operator.
     *
     * @param o
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/lte/">MongoDB Query operator: $lte</a>
     */
    public SearchQuery lte(Object o) {
        SearchQuery.put("$lte", o);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $gt} operator.
     *
     * @param o
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/gt/">MongoDB Query operator: $gt</a>
     */
    public SearchQuery gt(Object o) {
        SearchQuery.put("$gt", o);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $gte} operator.
     *
     * @param o
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/gte/">MongoDB Query operator: $gte</a>
     */
    public SearchQuery gte(Object o) {
        SearchQuery.put("$gte", o);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $in} operator.
     *
     * @param o the values to match against
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/in/">MongoDB Query operator: $in</a>
     */
    public SearchQuery in(Object... o) {
        if (o.length > 1 && o[1] instanceof Collection) {
            throw new RuntimeException(
                    "You can only pass in one argument of type " + o[1].getClass().getName());
        }
        SearchQuery.put("$in", Arrays.asList(o));
        return this;
    }

    /**
     * Creates a criterion using the {@literal $in} operator.
     *
     * @param c the collection containing the values to match against
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/in/">MongoDB Query operator: $in</a>
     */
    public SearchQuery in(Collection<?> c) {
        SearchQuery.put("$in", c);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $nin} operator.
     *
     * @param o
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/nin/">MongoDB Query operator: $nin</a>
     */
    public SearchQuery nin(Object... o) {
        return nin(Arrays.asList(o));
    }

    /**
     * Creates a criterion using the {@literal $nin} operator.
     *
     * @param o
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/nin/">MongoDB Query operator: $nin</a>
     */
    public SearchQuery nin(Collection<?> o) {
        SearchQuery.put("$nin", o);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $mod} operator.
     *
     * @param value
     * @param remainder
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/mod/">MongoDB Query operator: $mod</a>
     */
    public SearchQuery mod(Number value, Number remainder) {
        List<Object> l = new ArrayList<Object>();
        l.add(value);
        l.add(remainder);
        SearchQuery.put("$mod", l);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $all} operator.
     *
     * @param o
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/all/">MongoDB Query operator: $all</a>
     */
    public SearchQuery all(Object... o) {
        return all(Arrays.asList(o));
    }

    /**
     * Creates a criterion using the {@literal $all} operator.
     *
     * @param o
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/all/">MongoDB Query operator: $all</a>
     */
    public SearchQuery all(Collection<?> o) {
        SearchQuery.put("$all", o);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $size} operator.
     *
     * @param s
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/size/">MongoDB Query operator: $size</a>
     */
    public SearchQuery size(int s) {
        SearchQuery.put("$size", s);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $exists} operator.
     *
     * @param b
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/exists/">MongoDB Query operator: $exists</a>
     */
    public SearchQuery exists(boolean b) {
        SearchQuery.put("$exists", b);
        return this;
    }

    /**
     * Creates a criterion using the {@literal $type} operator.
     *
     * @param t
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/type/">MongoDB Query operator: $type</a>
     */
    public SearchQuery type(int t) {
        SearchQuery.put("$type", t);
        return this;
    }


    /**
     * Creates a criterion using the {@literal $not} meta operator which affects the clause directly following
     *
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/not/">MongoDB Query operator: $not</a>
     */
    public SearchQuery not() {
        return not(null);
    }

    /**
     * Creates a criterion using the {@literal $not} operator.
     *
     * @param value
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/not/">MongoDB Query operator: $not</a>
     */
    private SearchQuery not(Object value) {
        SearchQuery.put("$not", value);
        return this;
    }

    /**
     * Creates a criterion using a {@literal $regex} operator.
     *
     * @param re
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/regex/">MongoDB Query operator: $regex</a>
     */
    public SearchQuery regex(String re) {
        return regex(re, null);
    }

    /**
     * Creates a criterion using a {@literal $regex} and {@literal $options} operator.
     *
     * @param re
     * @param options
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/regex/">MongoDB Query operator: $regex</a>
     */
    public SearchQuery regex(String re, String options) {
        return regex(toPattern(re, options));
    }

    /**
     * Syntactical sugar for {@link #(V)} making obvious that we create a regex predicate.
     *
     * @param pattern
     * @return
     */
    public SearchQuery regex(Pattern pattern) {

        if (pattern == null) {
            throw new IllegalArgumentException("Pattern must not be null!");
        }

        if (lastOperatorWasNot()) {
            return not(pattern);
        }

        if (this.isValue == null) {
            this.isValue = new ArrayList();
        }

        DocumentQuery regex = new SearchQuery().and("$regex").is(pattern.toString()).getSearchQueryObject();

        this.isValue.add(regex);
        return this;
    }

    private Pattern toPattern(String regex, String options) {
        if (regex == null) {
            throw new IllegalArgumentException("Regex string must not be null!");
        }

        return Pattern.compile(regex, options == null ? 0 : SearchUtil.regexFlags(options));
    }


    /**
     * Creates a criterion using the {@literal $elemMatch} operator
     *
     * @param c
     * @return
     * @see <a href="https://docs.mongodb.com/manual/reference/operator/query/elemMatch/">MongoDB Query operator:
     * $elemMatch</a>
     */
    public SearchQuery elemMatch(SearchQuery c) {
        SearchQuery.put("$elemMatch", c.getSearchQueryObject());
        return this;
    }

    /**
     * Creates an 'or' SearchQuery using the $or operator for all of the provided SearchQuery
     * <p>
     * Note that mongodb doesn't support an $or operator to be wrapped in a $not operator.
     * <p>
     *
     * @param SearchQuery
     * @throws IllegalArgumentException if {@link #orOperator(SearchQuery...)} follows a not() call directly.
     */
    public SearchQuery orOperator(SearchQuery... SearchQuery) {
        ArrayList bsonList = createSearchQueryList(SearchQuery);
        return registerSearchQueryChainElement(new SearchQuery("$or").is(bsonList));
    }

    /**
     * Creates a 'nor' SearchQuery using the $nor operator for all of the provided SearchQuery.
     * <p>
     * Note that mongodb doesn't support an $nor operator to be wrapped in a $not operator.
     * <p>
     *
     * @param SearchQuery
     * @throws IllegalArgumentException if {@link #norOperator(SearchQuery...)} follows a not() call directly.
     */
    public SearchQuery norOperator(SearchQuery... SearchQuery) {
        ArrayList bsonList = createSearchQueryList(SearchQuery);
        return registerSearchQueryChainElement(new SearchQuery("$nor").is(bsonList));
    }

    /**
     * Creates an 'and' SearchQuery using the $and operator for all of the provided SearchQuery.
     * <p>
     * Note that mongodb doesn't support an $and operator to be wrapped in a $not operator.
     * <p>
     *
     * @param SearchQuery
     * @throws IllegalArgumentException if {@link #andOperator(SearchQuery...)} follows a not() call directly.
     */
    public SearchQuery andOperator(SearchQuery... SearchQuery) {
        ArrayList bsonList = createSearchQueryList(SearchQuery);
        return registerSearchQueryChainElement(new SearchQuery("$and").is(bsonList));
    }

    private SearchQuery registerSearchQueryChainElement(SearchQuery SearchQuery) {

        if (lastOperatorWasNot()) {
            throw new IllegalArgumentException(
                    "operator $not is not allowed around SearchQuery chain element: " + SearchQuery.getSearchQueryObject());
        } else {
            SearchQueryChain.add(SearchQuery);
        }
        return this;
    }

    public String getKey() {
        return this.key;
    }

    public void setSearchQueryObject(DocumentQuery searchQuery){
        this.searchQueryObject = searchQuery;
    }

    public DocumentQuery getSearchQueryObject() {
        if(searchQueryObject!=null){
            return  searchQueryObject;
        }
        if (this.SearchQueryChain.size() == 1) {
            return SearchQueryChain.get(0).getSingleSearchQueryObject();
        } else if (SearchUtil.isEmpty(this.SearchQueryChain) && !SearchUtil.isEmpty(this.SearchQuery)) {
            return getSingleSearchQueryObject();
        } else {
            DocumentQuery SearchQueryObject = new DocumentQuery();
            for (SearchQuery c : this.SearchQueryChain) {
                DocumentQuery document = c.getSingleSearchQueryObject();
                for (String k : document.keySet()) {
                    setValue(SearchQueryObject, k, document.get(k));
                }
            }
            return SearchQueryObject;
        }
    }

    protected DocumentQuery getSingleSearchQueryObject() {

        DocumentQuery document = new DocumentQuery();
        boolean not = false;

        for (Entry<String, Object> entry : SearchQuery.entrySet()) {

            String key = entry.getKey();
            Object value = entry.getValue();

//            if (requiresGeoJsonFormat(value)) {
//                value = new DocumentQuery("$geometry", value);
//            }

            if (not) {
                DocumentQuery notDocument = new DocumentQuery();
                notDocument.put(key, value);
                document.put("$not", notDocument);
                not = false;
            } else {
                if ("$not".equals(key) && value == null) {
                    not = true;
                } else {
                    document.put(key, value);
                }
            }
        }

        if (!SearchUtil.hasText(this.key)) {
            if (not) {
                return new DocumentQuery("$not", document);
            }
            return document;
        }

        DocumentQuery querySearchQuery = new DocumentQuery();

        if (isValue != null) {
            querySearchQuery.put(this.key, this.isValue.get(0));
            querySearchQuery.putAll(document);
        } else {
            querySearchQuery.put(this.key, document);
        }

        return querySearchQuery;
    }

    private ArrayList createSearchQueryList(SearchQuery[] SearchQuery) {
        ArrayList bsonList = new ArrayList();
        for (SearchQuery c : SearchQuery) {
            bsonList.add(c.getSearchQueryObject());
        }
        return bsonList;
    }

    private void setValue(DocumentQuery document, String key, Object value) {
        Object existing = document.get(key);
        if (existing == null) {
            document.put(key, value);
        } else {
            throw new RuntimeException("Due to limitations of the com.mongodb.BasicDocument, "
                    + "you can't add a second '" + key + "' expression specified as '" + key + " : " + value + "'. "
                    + "SearchQuery already contains '" + key + " : " + existing + "'.");
        }
    }


}
